#include <string.h>
#include <stdlib.h>
#include <unistd.h>  
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <pthread.h>
#include <curses.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/sendfile.h>
#include <signal.h>
#include <sys/stat.h>
#include <errno.h>

#define LISTENQ 2
#define MAXLINE 1024
#define BUFFERSIZE 255

WINDOW *top, *bottom;

pthread_t s_thread, l_thread;

char g_nick[MAXLINE] = "unknown_user";
int g_port = 13333;
int g_fd = 0; 
int g_listenfd = 0;
int filecomming = 0;
int maxx, maxy;
char g_color;
char g_is_connected = 0;
char g_helptext[] = "Possible commands are:\n/connect <port> <ip-address> - to making connection to another client\n\
/disconnect - to disconnecting\n/nick - to showing your current nickname\n/setnick <new nick> - to setting new nick\n\
/logpath - to showing your current logging path\n/setlogpath <new filename> - to changing your current loggin path\n\
/port <new port number> - to change the listening port for new connections\n/sendfile <filename> - to send a file\n/help - to showing this help text";

// readline() and writen() are utility methods for 
// reading/writing from/to a file descriptor.
void writen(int fd, void *ptr, size_t nbytes);
int readline( int fd, char *bufptr, size_t len );

int changesetup(char *type, char *value);
int logger(struct tm *timeinfo, char *nick, char *message);
int createsetup();

// Pre declaration of thread functions
void* server_thread();
void* listen_thread();

void sig_handler(int signo)
{
	if (g_is_connected == 0) {
		pthread_cancel(l_thread);
		if( close(g_listenfd) )
			syslog(0, "Closing listenfd error.\n");
	}
	else {
		pthread_cancel(s_thread);
		if( close(g_fd) )
			syslog(0, "Closing fd error.\n");
	}
	endwin();
	exit(0);
}

// This function prints to top window on the screen
int window_update(WINDOW *win, void *data) 
{
	if (g_color == 1)
		wattron(win, COLOR_PAIR(1));
	else if (g_color == 2)
		wattron(win, COLOR_PAIR(2));
	else
		wattron(win, COLOR_PAIR(3));
	wprintw(win,"%s", (char*)data ); wrefresh(win);
	if (g_color == 1)
		wattroff(win, COLOR_PAIR(1));
	else if (g_color == 2)
		wattroff(win, COLOR_PAIR(2));
	else
		wattroff(win, COLOR_PAIR(3));
	return 0;
}
    
// This function handles the CLI
void client()
{
	char buff[MAXLINE], input[maxx], cmd[MAXLINE], param[MAXLINE], param2[MAXLINE];
	time_t rawtime;
	struct tm *now;
	struct sockaddr_in servaddr;
	int rc, n;
	
	FILE *file;
	char line_buffer[BUFSIZ];
	char *setup = "settings.txt";

	if(file = fopen(setup, "r")){
		fgets(line_buffer, sizeof(line_buffer), file);
		fclose(file);
		line_buffer[strlen(line_buffer)-1] = 0;
		strcpy(g_nick, line_buffer);
	}

	else{
		createsetup();
		file = fopen(setup, "r");
		fgets(line_buffer, sizeof(line_buffer), file);
		fclose(file);
		line_buffer[strlen(line_buffer)-1] = 0;
		strcpy(g_nick, line_buffer);
	}
	
	while (1)
	{
		werase(bottom);
		wmove(bottom,0,0);
		wprintw(bottom,g_nick ); 
		wprintw(bottom, "> " );
		wrefresh(bottom); // Update contents to bottom window
		fflush(stdin);
		wgetnstr(bottom,input,sizeof(input)); // Read user input from bottom window
		
		// If getting some command or if not connected print help message for any input
		if ( (input[0] == '/') || g_is_connected == 0) {
			sscanf(input, "%s%s%s", cmd, param, param2);			
			// Change settings here, or connect or disconnect
			if ( !strcmp(cmd, "/disconnect") ) {
				if (g_is_connected == 0) {
					snprintf(buff, MAXLINE, "Not connected.\n");
				}
				else {
					snprintf(buff, MAXLINE, "%s\n", "/disconnect");
					writen(g_fd, buff, strlen(buff));
					g_is_connected = 0;
					pthread_cancel(s_thread);
					if( close(g_fd) )
						syslog(0, "Closing fd error.\n");
					snprintf(buff, MAXLINE, "Disconnected.\n");
					if ( (rc = pthread_create( &l_thread, NULL, &listen_thread, NULL)) )
						syslog(0, "Can't create listening thread.\n");
				}
				g_color = 3; // Colour for printing informative messages to own top window
				use_window( top, window_update, (void*)buff ); // Write buffer to own top window
			}
			else if ( !strcmp(cmd, "/connect") ) {
				
				if (g_is_connected == 1) {
					snprintf(buff, MAXLINE, "Already connected.\n");
					g_color = 3; // Colour for printing informative messages to own top window
					use_window( top, window_update, (void*)buff ); // Write buffer to own top window
				}		
				else {
					pthread_cancel(l_thread);
					g_fd = socket(AF_INET, SOCK_STREAM, 0);
					bzero(&servaddr, sizeof(servaddr));
					servaddr.sin_family = AF_INET;
					
					servaddr.sin_port = htons(atoi(param));
					if (inet_pton(AF_INET, param2, &servaddr.sin_addr) != 1)
						break;
					connect(g_fd, (struct sockaddr *) &servaddr, sizeof(servaddr));
					
					// Create a thread for reading incoming messages
					if ( (rc = pthread_create( &s_thread, NULL, &server_thread, NULL)) )
					{
						syslog(0, "Can't create server thread.\n");
					}	
				}
			}
			else if ( !strcmp(cmd, "/port") ) {
				g_port = atoi(param);
				pthread_cancel(l_thread);
				if ( (rc = pthread_create( &l_thread, NULL, &listen_thread, NULL)) )
				{
					syslog(0, "Can't create listening thread.\n");
				}
			}
			
			else if ( !strcmp(cmd, "/nick") ) {
				file = fopen(setup, "r");
				fgets(line_buffer, sizeof(line_buffer), file);
				fclose(file);
				snprintf(buff, MAXLINE, "%s\n", line_buffer);
				use_window( top, window_update, (void*)buff);
			}
			else if ( !strcmp(cmd, "/setnick") ) {
				changesetup(cmd, param);
				strcpy(g_nick, param);
			}
			else if ( !strcmp(cmd, "/logpath") ) {
				int i = 0;
				file = fopen(setup, "r");
				while(i < 2) {
					fgets(line_buffer, sizeof(line_buffer), file);
					i++;
				}
				fclose(file);

				snprintf(buff, MAXLINE, "%s\n", line_buffer);
				use_window( top, window_update, (void*)buff);
			}
			else if ( !strcmp(cmd, "/setlogpath") ) {
				changesetup(cmd, param);
			}
			else if ( !strcmp(cmd, "/help") ) {
				snprintf(buff, MAXLINE, "%s\n", g_helptext);
				g_color = 3; // Colour for printing informative messages to own top window
				use_window( top, window_update, (void*)buff);
			}
			else if(!strcmp(cmd, "/sendfile")){
				FILE *f = fopen(param, "r+b");
				if ( f == NULL ) {
					snprintf(buff, MAXLINE, "File not found.\n");
					g_color = 3; // Colour for printing informative messages to own top window
					use_window( top, window_update, (void*)buff);
				}
				else {
					struct stat stat_buf;
					int filefd = fileno(f);
					snprintf(buff, MAXLINE, "Sending file %s\n", param);
					g_color = 3;
					use_window( top, window_update, (void*)buff);
					fstat(filefd, &stat_buf);
					snprintf(buff, MAXLINE, "%s %s %lu\n", "/finc", param, (unsigned long)(stat_buf.st_size));
					writen(g_fd, buff, strlen(buff));
					sleep(1); // Wait for receiver to handle the filename and filesize before sending
					off_t offset = 0;
					if( sendfile(g_fd, filefd, NULL, stat_buf.st_size) < 0 ){
						syslog(0, "Error reading file to send.");
					}
				}
			}
			else {
				snprintf(buff, MAXLINE, "Please type /help for possible commands.\n"); 
				g_color = 3; // Colour for printing informative messages to own top window
				use_window( top, window_update, (void*)buff);
			}
		}			
		else {
			rawtime = time(NULL);
			now = localtime( &rawtime );
			
			snprintf(buff, MAXLINE, "%s %s\n", g_nick, input );
			writen(g_fd, buff, strlen(buff)); // Send buffer to other client
			snprintf(buff, MAXLINE, "%02d:%02d:%02d %s %s\n", 
				now->tm_hour, now->tm_min, now->tm_sec, g_nick, input);
			logger(now, g_nick, input);
			g_color = 1; // Colour for printing in own messages to own top window
			use_window( top, window_update, (void*)buff ); // Write buffer to own top window
		}
	}
}

// This thread reads and prints incoming messages
void* server_thread() 
{
	size_t n;	
	char line[MAXLINE], buff[MAXLINE];
	time_t rawtime;
	struct tm *now; 
	int rc, y, i;
	void *buf[BUFFERSIZE];
	FILE *f;
	char filename[MAXLINE] = "";
	unsigned long filesize = 0;
	
	g_is_connected = 1;
	g_color = 3; // Colour for printing informative messages to own top window
	strcpy(buff, "Client connected.\n");
	use_window(top, window_update, (void*)buff);
	
	while (1) {
		if (filecomming == 0) {
			if ( (n = readline(g_fd, line, MAXLINE)) == 0)	{
				return;	// Connection closed by other end
			}
			
			if (!strncmp(line, "/finc\n", 5)){
				sscanf(line, "%s%s%lu", buff, filename, &filesize);
				filecomming = 1;
			}
			else if (!strncmp(line, "/disconnect\n", 11)){
				g_is_connected = 0;
				snprintf(buff, MAXLINE, "Disconnected.\n");
				g_color = 3; // Colour for printing informative messages to own top window
				use_window( top, window_update, (void*)buff);
				pthread_cancel(s_thread);
				if( close(g_fd) )
					syslog(0, "Closing fd error.\n");
				if ( (rc = pthread_create( &l_thread, NULL, &listen_thread, NULL)) )
					syslog(0, "Can't create listening thread.\n");
			}
			else {
				rawtime = time(NULL);
				now = localtime( &rawtime );
				snprintf(buff, MAXLINE, "%02d:%02d:%02d %s", now->tm_hour, now->tm_min, now->tm_sec, line);
				g_color = 2; // Colour for printing other clients messages to own top window
				logger(now, "none", line);
				use_window(top, window_update, (void*)buff); // Write buffer to own top window
			}
		}
		else {
			snprintf(buff, MAXLINE, "%s%s\n", "Receiving file ", filename);
			g_color = 3;
			use_window( top, window_update, (void*)buff);
			int amountOfBuffers = filesize / BUFFERSIZE;
			if (!( filesize % BUFFERSIZE == 0 )) // Calculate how many times we have to read for receiving the file
				++amountOfBuffers;
			f = fopen(filename, "a+b");		
			for( i = 0; i < amountOfBuffers; ++i ){
				y = read(g_fd, buf, BUFFERSIZE);
				if(y < 0) {
					syslog(0, "File reading error.");
					filecomming = 0;
					break;
				}
				if ((write(fileno(f), buf, y)) < 0) {
					syslog(0, "File writing error.");
					filecomming = 0;
					break;
				}
			}
			snprintf(buff, MAXLINE, "File %s saved on disk.\n", filename);
			use_window( top, window_update, (void*)buff);
			fclose(f);
			filecomming = 0;
		}
	}
	if( close(g_fd) )
		syslog(0, "Closing fd error.\n");
	return (void *)0;
}

// This thread listen to connection establishment from other client
void* listen_thread()
{
	int n, rc;
	char buff[MAXLINE];
	struct sockaddr_in	servaddr;
	
	g_listenfd = socket(AF_INET, SOCK_STREAM, 0);
	
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(g_port);
	
	bind(g_listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
	listen(g_listenfd, LISTENQ);
	g_fd = accept(g_listenfd, NULL, NULL);
	
	// Create a thread for reading incoming messages
	if ( (rc = pthread_create( &s_thread, NULL, &server_thread, NULL)) )
	{
		syslog(0, "Can't create server thread.\n");
	}
	if( close(g_listenfd) )
		syslog(0, "Closing listenfd error.\n");
	return (void *)0;
}

// Main-function does the initialization
int main(int argc, char **argv)
{
	int rc;
	
	if (argc > 1) {
		printf("usage: chatcli\n");
		return 1;
	}
	
	if (signal(SIGINT, sig_handler) == SIG_ERR)
		syslog(0, "Can't catch SIGINT");
	if (signal(SIGQUIT, sig_handler) == SIG_ERR)
		syslog(0, "Can't catch SIGQUIT");
	if (signal(SIGTERM, sig_handler) == SIG_ERR)
		syslog(0, "Can't catch SIGTERM");
	if (signal(SIGSEGV, sig_handler) == SIG_ERR)
		syslog(0, "Can't catch SIGSEGV");
	
	initscr();  // Initialize ncurses graphics
	
	if (!has_colors()) {
		syslog(0, "Terminal doesn't have colors.\n");
		return 0;
	}
	// Initialize colors
	start_color();
	init_pair(1, COLOR_RED, COLOR_BLACK);
	init_pair(2, COLOR_BLUE, COLOR_BLACK);
	init_pair(3, COLOR_GREEN, COLOR_BLACK);
	
	getmaxyx(stdscr, maxy, maxx);
	top = newwin(maxy-1,maxx,0,0);
	wsetscrreg(top,0,maxy-1);
	idlok(top,1); 
	scrollok(top,1);
	bottom = newwin(1,maxx,maxy-1,0);
	
	if ( (rc = pthread_create( &l_thread, NULL, &listen_thread, NULL)) )
	{
		syslog(0, "Can't create listening thread.\n");
	}
    client();
	endwin();
	exit(0);
}